package idPool8Bit

import (
	"encoding/binary"
	"errors"
	"fmt"
	"math/rand"
	"os"
	"time"
	"unsafe"
)

// size /byte

const (
	IDPoolWorkIDSize       = 1
	IDPoolIterateNumSize   = 1
	IDPoolCurrentBlockSize = 4
	IDPoolConsume          = 8
	IDPoolPrefixOffsetSize = IDPoolWorkIDSize + IDPoolIterateNumSize + IDPoolCurrentBlockSize + IDPoolConsume
)

type Block uint64

var (
	IDPoolFullyUsedErr = errors.New("the id pool is fully used")
)

func (b Block) getIndexesOfZeros() []int {
	var indexes []int
	for i := 0; i < 64; i++ {
		if (b>>i)&1 == 0 {
			indexes = append(indexes, i)
		}
	}
	return indexes
}

func (b Block) getNumAndNewBlock(workID uint8, currentBlock uint32) (uint64, Block) {
	ones := b.getIndexesOfZeros()
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	_subscript := ones[r.Intn(len(ones))]
	// set subscript Block to 0
	return uint64(workID)*maxCurrentWork8BitIDSize + uint64(currentBlock*64) + uint64(64-_subscript), b | (1 << _subscript)
}

// ExposedIDPool the idPoolRecord can not expose, we can copy and expose this pool
type ExposedIDPool struct {
	WorkID       uint8
	IterateNum   int8
	CurrentBlock uint32 // current Block
	Consume      uint64
	Blocks       []Block
}

func (p ExposedIDPool) JustExpose() {}

type Exposed interface {
	JustExpose()
}

type idPoolRecord struct {
	WorkID       uint8
	IterateNum   int8
	CurrentBlock uint32 // current Block
	Consume      uint64
	Blocks       []Block
}

func (p *idPoolRecord) getIDPoolSize() int {
	return int(unsafe.Sizeof(*p)) + len(p.Blocks)*int(unsafe.Sizeof(Block(0)))
}

func (p *idPoolRecord) serialize() ([]byte, error) {
	size := p.getIDPoolSize()
	buf := make([]byte, size)

	// 序列化 idPoolRecord 基本字段
	buf[0] = p.WorkID
	buf[1] = byte(p.IterateNum)
	binary.LittleEndian.PutUint32(buf[2:], p.CurrentBlock)
	binary.LittleEndian.PutUint64(buf[6:], p.Consume)

	// Serialize blocks
	offset := IDPoolPrefixOffsetSize
	for i := 0; i < len(p.Blocks); i++ {
		if offset >= size {
			return nil, errors.New(fmt.Sprintf("out of range value %d", offset))
		}
		binary.LittleEndian.PutUint64(buf[offset:], uint64(p.Blocks[i]))
		offset += blockSize
	}
	return buf, nil
}

func (p *idPoolRecord) deserializeIDPool(buf []byte) error {

	if len(buf) != p.getIDPoolSize() {
		return errors.New("idPoolRecord deserializeIDPool: invalid buffer size")
	}

	p.WorkID = buf[0]
	p.IterateNum = int8(buf[1])
	p.CurrentBlock = binary.LittleEndian.Uint32(buf[2:])
	p.Consume = binary.LittleEndian.Uint64(buf[6:])

	// Deserialize blocks
	offset := IDPoolPrefixOffsetSize
	blocksLen := (len(buf) - offset) / blockSize

	p.Blocks = make([]Block, blocksLen)
	for i := 0; i < blocksLen; i++ {
		p.Blocks[i] = Block(binary.LittleEndian.Uint64(buf[offset:]))
		offset += blockSize
	}
	return nil
}

func fileExists(fileName string) bool {
	_, err := os.Stat(fileName)
	if os.IsNotExist(err) {
		return false
	}
	return err == nil
}

func (p *idPoolRecord) initWalLogFile(fileName string) (data []byte, err error) {
	exists := fileExists(fileName)
	if exists {
		return nil, errors.New("current record file exists")
	}

	file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, 0644)
	if err != nil {
		return nil, err
	}

	// Initialization file content
	serialize, err := p.serialize()
	if err != nil {
		return nil, err
	}
	if _, err = file.WriteAt(serialize, 0); err != nil {
		return nil, err
	}

	// Ensure data is synced to disk
	if err = file.Sync(); err != nil {
		return nil, err
	}

	defer file.Close()
	return p.mmapFile(fileName, file)
}

func (p *idPoolRecord) mmapFile(fileName string, file *os.File) (data []byte, err error) {
	// Calculate idPool memory size
	size := p.getIDPoolSize()

	// Make sure the file is of sufficient size
	stat, err := file.Stat()
	if err != nil {
		return nil, err
	}
	if stat.Size() < int64(size) {
		if err = file.Truncate(int64(size)); err != nil {
			return nil, err
		}
	}

	// Map files to memory
	data, err = mmapFile(fileName, size)
	if err != nil {
		return nil, err
	}

	return data, nil
}

func (p *idPoolRecord) recoverFormWalLog(fileName string) (data []byte, err error) {
	exists := fileExists(fileName)
	if !exists {
		return nil, errors.New("current record file not exists")
	}
	file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, 0644)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	size := p.getIDPoolSize()
	recordData := make([]byte, size)
	readLen, err := file.Read(recordData)
	if err != nil {
		return nil, err
	}
	if readLen != size {
		return nil, errors.New("invalid file size")
	}

	if err = p.deserializeIDPool(recordData); err != nil {
		return nil, err
	}

	// Map files to memory
	return p.mmapFile(fileName, file)
}

func (p *idPoolRecord) getNextID() (nextBlock uint32, nextIterateNum int8, id uint64, newBlock Block, nextIterate bool, err error) {
	if uint64(p.CurrentBlock+1) < subscriptLen {
		// move CurrentBlock out_subscript to next
		nextBlock = p.CurrentBlock + 1
		nextIterateNum = p.IterateNum
	} else {
		// reset current
		nextIterateNum = p.IterateNum + 1
		if nextIterateNum >= maxSubscript {
			return 0, 0, 0, 0, false, IDPoolFullyUsedErr
		}
		nextIterate = true
		nextBlock = 0
	}
	// maxUint64 : this Block all to used
	if uint64(p.Blocks[nextBlock]) >= maxUint64 {
		return 0, 0, 0, 0, false, IDPoolFullyUsedErr
	}
	id, newBlock = p.Blocks[nextBlock].getNumAndNewBlock(p.WorkID, nextBlock)
	return
}

func (p *idPoolRecord) move(nextBlock uint32, nextIterateNum int8, newBlock Block) error {
	// double check
	if nextIterateNum >= maxSubscript {
		return errors.New("the id pool is fully used ")
	}
	// maxUint64 : this Block all to used
	if uint64(p.Blocks[nextBlock]) >= maxUint64 {
		return errors.New("the id pool is fully used ")

	}

	p.CurrentBlock = nextBlock
	p.IterateNum = nextIterateNum
	p.Blocks[nextBlock] = newBlock
	p.Consume++
	return nil
}

func (p *idPoolRecord) nextID() (id uint64, err error) {
	if uint64(p.CurrentBlock+1) < subscriptLen {
		// move CurrentBlock out_subscript to next
		p.CurrentBlock++
	} else {
		// reset current
		p.IterateNum++
		if p.IterateNum >= maxSubscript {
			return 0, IDPoolFullyUsedErr
		}
		p.CurrentBlock = 0
	}

	// maxUint64 : this Block all to used
	if uint64(p.Blocks[p.CurrentBlock]) < maxUint64 {
		id, p.Blocks[p.CurrentBlock] = p.Blocks[p.CurrentBlock].getNumAndNewBlock(p.WorkID, p.CurrentBlock)
		p.Consume++
		return id, nil
	}

	return 0, IDPoolFullyUsedErr
}

func (p *idPoolRecord) nextIDWithInfo() (id uint64, currentBlock uint32, iterateNum int8, block Block, err error) {
	if uint64(p.CurrentBlock+1) < subscriptLen {
		// move CurrentBlock out_subscript to next
		p.CurrentBlock++
	} else {
		// reset current
		p.IterateNum++
		if p.IterateNum >= maxSubscript {
			return 0, 0, 0, 0, IDPoolFullyUsedErr
		}
		p.CurrentBlock = 0
	}

	// maxUint64 : this Block all to used
	if uint64(p.Blocks[p.CurrentBlock]) < maxUint64 {
		id, p.Blocks[p.CurrentBlock] = p.Blocks[p.CurrentBlock].getNumAndNewBlock(p.WorkID, p.CurrentBlock)
		p.Consume++
		return id, p.CurrentBlock, p.IterateNum, p.Blocks[p.CurrentBlock], nil
	}

	return 0, 0, 0, 0, IDPoolFullyUsedErr
}
